//-------------------------------------------------------------------
//	Copyright (c) 2012-2014 Zhirnov Andrey
//	This file is part of the "UXGEN-ES" project.
//	See copyright notice in "Engine.h".
//-------------------------------------------------------------------

#pragma once

#include "Utils/Texture/Image/Image.h"
#include "Utils/Texture/Image/PixelFormatToColorFormat.h"

#include <DevIL/IL.h>
#include <DevIL/ILU.h>


namespace Engine
{
namespace Utils
{

	struct EImagePartsOrient
	{
		enum type
		{
			AUTO		= 0,
			HORIZONTAL	= 1,
			VERTICAL 	= 2,
		};
	};


	//
	// Image Loader
	//

	struct ImageLoader
	{
	// methods
	public:
		static bool Load (uni_c_string filename, OUT Image &img, float scale, bool toPOT)
		{
			return _LoadIL( filename, img, scale, toPOT );
		}

		static bool Load (const uni_array< const string > &filenames, OUT Image &img,
						  const color4f &borderColor, const uvec2 &borderSize,
						  float scale, bool toPOT, EImagePartsOrient::type orient = EImagePartsOrient::AUTO)
		{
			return _LoadArrayIL( filenames, img, borderColor, borderSize, scale, toPOT, orient );
		}


		static bool Convert (OUT Image &outImage, const Image &inImage, EPixelFormat::type newFormat);

		static bool CopyTo (INOUT Image &dstImage, const uvec3 &dstOffset, const Image &srcImage);


	private:
		struct ILimage
		{
			ILuint	id;
			///
			ILimage (): id(0)				{ ilGenImages( 1, &id ); }
			ILimage (ILimage &i): id(i.id)	{ i.id = 0; }
			~ILimage ()						{ ilDeleteImages( 1, &id ); }
			void Bind ()					{ ilBindImage( id ); }
			void Unbind ()					{ ilBindImage(  0 ); }

			static void InitDevIL ()
			{
				static bool		inited = false;

				if ( not inited )
				{
					inited = true;
					ilInit();
					iluInit();
				}
			}
		};


		static EPixelFormat::type _GetILFormat (ILint format, ILint type);
		
		static bool _LoadIL (uni_c_string filename, Image &img, float scale, bool toPOT);

		static bool _LoadArrayIL (const uni_array< const string > &filenames, Image &img,
								  const color4f &borderColor, const uvec2 &borderSize,
								  float scale, bool toPOT, EImagePartsOrient::type orient);

		static bool _LoadTex (uni_c_string filename, Image &img);

		static uvec3 _GetNewDim (const uvec3 &dim, float scale, bool toPOT);

		static bool _LoadImageIL (const ILimage &src, Image &dst);
	};

	

	// _GetILFormat
	inline EPixelFormat::type ImageLoader::_GetILFormat (ILint format, ILint type)
	{
		switch ( format )
		{
			case IL_ALPHA :
				switch ( type )
				{
					case IL_FLOAT :
						return EPixelFormat::ALPHA32F;

					case IL_HALF :
						return EPixelFormat::ALPHA16F;

					default :
						if ( type != IL_UNSIGNED_BYTE )
						{
							type = IL_UNSIGNED_BYTE;
							ilConvertImage( format, type );
						}
						return EPixelFormat::ALPHA8;
				}
				break;

			case IL_BGR :
				format = IL_RGB;
				ilConvertImage( format, type );

			case IL_RGB :
				switch ( type )
				{
					case IL_FLOAT :
						return EPixelFormat::RGB32F;

					case IL_HALF :
						return EPixelFormat::RGB16F;

					default :
						if ( type != IL_UNSIGNED_BYTE )
						{
							type = IL_UNSIGNED_BYTE;
							ilConvertImage( format, type );
						}
						return EPixelFormat::RGB8;
				}
				break;


			default :
				if ( format != IL_RGBA )
				{
					format = IL_RGBA;
					ilConvertImage( format, type );
				}
				switch ( type )
				{
					case IL_FLOAT :
						return EPixelFormat::RGBA32F;

					case IL_HALF : 
						return EPixelFormat::RGBA16F;

					default :
						if ( type != IL_UNSIGNED_BYTE )
						{
							type = IL_UNSIGNED_BYTE;
							ilConvertImage( format, type );
						}
						return EPixelFormat::RGBA8;
				}
				break;
		}
		return EPixelFormat::_UNKNOWN;
	}

	
	// _GetNewDim
	inline uvec3 ImageLoader::_GetNewDim (const uvec3 &dim, float scale, bool toPOT)
	{
		uvec3	new_dim		= dim;
		bool	need_scale	= false;

		if ( scale != 1.0f ) {
			new_dim		= Round<uint>( dim.To<vec3>() * scale );
		}

		if ( toPOT ) {
			new_dim		= NearPowerOfTwo( new_dim );
		}
		return new_dim;
	}


	// _LoadIL
	inline bool ImageLoader::_LoadIL (uni_c_string filename, Image &image, float scale, bool toPOT)
	{
		ILimage::InitDevIL();
		
		CHECK_ERR( FileSystem::IsFileExist( filename.cstr() ) );

		ILimage		img;

		img.Bind();
		CHECK_ERR( ilLoadImage( filename.cstr() ) == IL_TRUE );
		
		// flip some images //
		uni_c_string	file_ext = FileAddressUtils::GetExtension( filename );

		if ( file_ext.ICmp( "bmp" ) )
		{
			iluFlipImage();
		}


		EPixelFormat::type	format	= _GetILFormat( ilGetInteger (IL_IMAGE_FORMAT ), ilGetInteger( IL_IMAGE_TYPE ) );

		CHECK_ERR( format != EPixelFormat::_UNKNOWN );

		// scale //
		uvec3	dim			= uvec3( ilGetInteger( IL_IMAGE_WIDTH ),
									 ilGetInteger( IL_IMAGE_HEIGHT ),
									 ilGetInteger( IL_IMAGE_DEPTH ) );
		uvec3	new_dim		= _GetNewDim( dim, scale, toPOT );
		bool	need_scale	= Any( new_dim != dim );

		if ( need_scale )
		{
			iluScale( new_dim.x, new_dim.y, new_dim.z );
			
			uvec3	dim	= uvec3( ilGetInteger( IL_IMAGE_WIDTH ),
								 ilGetInteger( IL_IMAGE_HEIGHT ),
								 ilGetInteger( IL_IMAGE_DEPTH ) );

			ASSERT( All( dim == new_dim ) );
		}
		
		CHECK_ERR( _LoadImageIL( img, image ) );

		img.Unbind();

		return true;
	}

	
	// _LoadImageIL
	inline bool ImageLoader::_LoadImageIL (const ILimage &src, Image &dst)
	{
		EPixelFormat::type	format	= _GetILFormat( ilGetInteger (IL_IMAGE_FORMAT ), ilGetInteger( IL_IMAGE_TYPE ) );

		uvec3	dim			= uvec3( ilGetInteger( IL_IMAGE_WIDTH ),
									 ilGetInteger( IL_IMAGE_HEIGHT ),
									 ilGetInteger( IL_IMAGE_DEPTH ) );
		int		bitPerPixel = ilGetInteger( IL_IMAGE_BITS_PER_PIXEL );
		uint	channel		= ilGetInteger( IL_IMAGE_CHANNELS );
		uint	max_size	= (dim.x * dim.y * dim.z * bitPerPixel) >> 3;

		uint	max_mipmaps	= UXMathExt::GetNumberOfMipmaps( dim ) + 1;
		uint	mipmaps	= Max( ilGetInteger( IL_NUM_MIPMAPS ), 1 );
		
		ASSERT( mipmaps <= max_mipmaps );
		
		dst._dim			= dim;
		dst._pixelFormat	= format;

		for (uint i = 0; i < mipmaps; ++i)
		{
			ilActiveMipmap( i );

			const uint		size = ilGetInteger( IL_IMAGE_SIZE_OF_DATA );
			const uint8 *	data_ptr = ilGetData();
			
			dim = uvec3( ilGetInteger( IL_IMAGE_WIDTH ),
						 ilGetInteger( IL_IMAGE_HEIGHT ),
						 ilGetInteger( IL_IMAGE_DEPTH ) );

			dst.AddMipmap( i, dim, uni_binbuffer_t( data_ptr, size ) );
		}
		return true;
	}

	
	// _LoadArrayIL
	inline bool ImageLoader::_LoadArrayIL (const uni_array< const string > &filenames, OUT Image &image,
											const color4f &borderColor, const uvec2 &borderSize,
											float scale, bool toPOT, EImagePartsOrient::type orient)
	{
		struct ImgInfo
		{
			uvec3				dim;
			ILint				format,
								type,
								bpp;
			EPixelFormat::type	fmt;
			///
			ImgInfo () :format(0), type(0), bpp(0), fmt(EPixelFormat::_UNKNOWN) {}

			bool CompareFormats (const ImgInfo &other) const {
				return format == other.format and type == other.type and bpp == other.bpp;
			}

			void Load ()
			{
				dim		= uvec3( ilGetInteger( IL_IMAGE_WIDTH ),
								 ilGetInteger( IL_IMAGE_HEIGHT ),
								 ilGetInteger( IL_IMAGE_DEPTH ) );
				bpp		= ilGetInteger(IL_IMAGE_BPP);
				format	= ilGetInteger(IL_IMAGE_FORMAT);
				type	= ilGetInteger(IL_IMAGE_TYPE);
			}
		};


		ILimage::InitDevIL();

		array< ILimage >	src_images( filenames.Count() );
		array< ImgInfo >	infos( filenames.Count() );

		FOR( i, src_images )
		{
			CHECK_ERR( FileSystem::IsFileExist( filenames[i].cstr() ) );

			src_images[i].Bind();
			
			CHECK_ERR( ilLoadImage( filenames[i].cstr() ) == IL_TRUE );
			
			// flip some images //
			uni_c_string	file_ext = FileAddressUtils::GetExtension( filenames[i] );

			if ( file_ext.ICmp( "bmp" ) )
			{
				iluFlipImage();
			}
			
			infos[i].fmt	= _GetILFormat( ilGetInteger (IL_IMAGE_FORMAT ), ilGetInteger( IL_IMAGE_TYPE ) );
			
			uvec3	dim			= uvec3( ilGetInteger( IL_IMAGE_WIDTH ),
										 ilGetInteger( IL_IMAGE_HEIGHT ),
										 ilGetInteger( IL_IMAGE_DEPTH ) );
			uvec3	new_dim		= _GetNewDim( dim, scale, toPOT );
			bool	need_scale	= Any( new_dim != dim );

			if ( need_scale )
			{
				iluScale( new_dim.x, new_dim.y, new_dim.z );
			}
			
			infos[i].Load();

			if ( i != 0 )
			{
				if ( not infos[i].CompareFormats( infos[i-1] ) )
				{
					WARNING( "image formats mismatch" );

					ilConvertImage( infos[i-1].format, infos[i-1].type );

					infos[i].Load();
					infos[i].fmt = infos[i-1].fmt;
				}

				if ( Any( infos[i].dim != infos[i-1].dim ) )
				{
					WARNING( "image sizes mismatch" );

					const uvec3 & new_dim = infos[i-1].dim;

					iluScale( new_dim.x, new_dim.y, new_dim.z );
					
					infos[i].Load();
				}
			}

			src_images[i].Unbind();
		}


		// copy to 1 image
		CHECK_ERR( infos.Front().dim.z <= 1 );	// only 2D supported

		const uvec2	dim		= infos.Front().dim.xy() + borderSize * 2;
		const uint	count	= infos.Count();
		const float	aspect	= float(dim.x) / float(dim.y);
		
		//ILimage	img;
		//img.Bind();

		if ( false ) //IsEven( count ) )
		{
			// near to quad
			

		}
		else
		{
			// one row or column
			uvec2	delta;
			uvec2	dst_dim;
			uvec2	offset = borderSize;

			if ( orient == EImagePartsOrient::VERTICAL or
				(orient == EImagePartsOrient::AUTO and aspect > 1.01f) )
			{
				// column
				dst_dim	= uvec2( dim.x, dim.y * count );
				delta	= uvec2( 0, dim.y );
			}
			else
			{
				// row
				dst_dim	= uvec2( dim.x*count, dim.y );
				delta	= uvec2( dim.x, 0 );
			}
			
			const EPixelFormat::type	format = infos.Front().fmt;

			ConvertImageDataFunc_t	conv = PixelFormatToColorFormat::GetConverterProc( format, format );
			FillImageFunc_t			fill = PixelFormatToColorFormat::GetFillProc( EPixelFormat::RGBA32F_NORM, format );

			CHECK_ERR( conv != null and fill != null );

			binary_array_t	data;
			const usize		size = PixelFormatToColorFormat::GetImageSize( uvec3( dst_dim, 1 ), 4, format );
			
			CHECK_ERR( size > 0 );
			CHECK_ALLOC( data.Resize( size, false ) );

			const UXMathExt::ColorFormat::RGBA32f_Norm	fill_color( borderColor );
			CHECK_ERR( fill( uvec3( dst_dim, 1 ), &fill_color, uvec3(0), uvec3( dst_dim, 1 ), data.ptr(), 4 ) );

			Image	tmp_img;

			FOR( i, infos )
			{
				tmp_img.Clear();

				src_images[i].Bind();
				
				CHECK_ERR( _LoadImageIL( src_images[i], tmp_img ) );

				CHECK_ERR( conv( infos[i].dim, uvec3(0), tmp_img.GetMipmapData(0).ptr(),
									uvec3( dst_dim, 1 ), uvec3( offset, 0 ), data.ptr(), 4, 4 ) );

				src_images[i].Unbind();

				offset += delta;
			}
			
			image.AddMipmap( 0, uvec3( dst_dim, 1 ), data );
			image._pixelFormat	= format;
			image._dim			= uvec3( dst_dim, 1 );
		}
		
		return true;
	}

	
	// _LoadTex
	inline bool ImageLoader::_LoadTex (uni_c_string filename, Image &img)
	{
		return false;
	}
	

	// Convert
	inline bool ImageLoader::Convert (OUT Image &outImage, const Image &inImage, EPixelFormat::type newFormat)
	{
		ConvertImageDataFunc_t proc = PixelFormatToColorFormat::GetConverterProc( inImage.PixelFormat(), newFormat );

		if ( proc == null )
			return false;

		const usize		size = PixelFormatToColorFormat::GetImageSize( inImage.Dimension(), 4, newFormat );
		binary_array_t	data;
		
		CHECK_ERR( size > 0 );
		CHECK_ALLOC( data.Resize( size, false ) );

		CHECK_ERR( proc( inImage.Dimension(), uvec3(0), inImage.GetMipmapData(0).ptr(), inImage.Dimension(), uvec3(0), data.ptr(), 4, 4 ) );

		outImage.AddMipmap( 0, inImage.Dimension(), data );
		outImage._pixelFormat	= newFormat;
		outImage._dim			= inImage.Dimension();

		return true;
	}


	// CopyTo
	inline bool ImageLoader::CopyTo (INOUT Image &dstImage, const uvec3 &dstOffset, const Image &srcImage)
	{
		ConvertImageDataFunc_t proc = PixelFormatToColorFormat::GetConverterProc( srcImage.PixelFormat(), dstImage.PixelFormat() );

		if ( proc == null )
			return false;

		TODO( "CopyTo" );
		return true;
	}


}	// Utils
}	// Engine